/*
 * Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2021, 2024 IBM Corporation. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Gordon Yorke - Initial development
//     10/01/2018: Will Dazey
//       - #253: Add support for embedded constructor results with CriteriaBuilder
//     10/25/2023: Tomas Kraus
//       - New Jakarta Persistence 3.2 Features
package org.eclipse.persistence.internal.jpa.querydef;

import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Selection;
import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.Type.PersistenceType;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.helper.BasicTypeHelperImpl;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl;
import org.eclipse.persistence.internal.jpa.metamodel.TypeImpl;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedGetConstructorFor;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReportQuery;

/**
 * <p>
 * <b>Purpose</b>: Contains the implementation of the CriteriaQuery interface of
 * the JPA criteria API.
 * <p>
 * <b>Description</b>: This is the container class for the components that
 * define a query.
 *
 * @see jakarta.persistence.criteria CriteriaQuery
 *
 * @author gyorke
 * @since EclipseLink 1.2
 */
public class CriteriaQueryImpl<T> extends AbstractQueryImpl<T> implements CriteriaQuery<T> {

    protected SelectionImpl<? extends T> selection;
    protected List<Order> orderBy;

    protected Set<FromImpl> joins;
    // Mark this query as part of the UNION/EXCEPT/INTERSECT, default is false
    private boolean isUnion = false;

    public CriteriaQueryImpl(Metamodel metamodel, ResultType queryResult, Class<T> result, CriteriaBuilderImpl queryBuilder) {
        super(metamodel, queryResult, queryBuilder, result);
    }

    @Override
    public CriteriaQuery<T> select(Selection<? extends T> selection) {
        this.selection = (SelectionImpl<? extends T>) selection;
        this.selection.findRootAndParameters(this);
        if (selection.isCompoundSelection()) {
            //bug 366386: validate that aliases are not reused
            if (this.selection.isCompoundSelection() && ((CompoundSelectionImpl)this.selection).getDuplicateAliasNames() != null) {
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa_criteriaapi_alias_reused",
                        new Object[] { ((CompoundSelectionImpl)this.selection).getDuplicateAliasNames() }));
            }
            if (selection.getJavaType().equals(Tuple.class)) {
                this.queryResult = ResultType.TUPLE;
                this.queryType = (Class<T>) Tuple.class;
            } else if (((InternalSelection) selection).isConstructor()) {
                Selection[] selectArray = selection.getCompoundSelectionItems().toArray(new Selection[0]);
                populateAndSetConstructorSelection((ConstructorSelectionImpl<T>)selection, (Class<T>) this.selection.getJavaType(), selectArray);
                this.queryType = (Class<T>) selection.getJavaType();
            } else {
                this.queryResult = ResultType.OBJECT_ARRAY;
                this.queryType = (Class<T>) ClassConstants.AOBJECT;
            }
        } else {
            // Update query type only when it's not null in selection argument.
            Class<?> queryType = selection.getJavaType();
            if (queryType != null) {
                this.queryType = (Class<T>) queryType;
            }
            TypeImpl type = ((MetamodelImpl)this.metamodel).getType(this.queryType);
            if (type != null && type.getPersistenceType().equals(PersistenceType.ENTITY)) {
                this.queryResult = ResultType.ENTITY; // this will be a selection item in a report query
            } else {
                this.queryResult = ResultType.OTHER;
            }
        }
        return this;
    }

    /**
     * Specify the items that are to be returned in the query result. Replaces
     * the previously specified selection(s), if any.
     * <p>
     * The type of the result of the query execution depends on the
     * specification of the criteria query object as well as the arguments to
     * the multiselect method as follows:
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Tuple&gt;, a Tuple object
     * corresponding to the arguments of the multiselect method will be
     * instantiated and returned for each row that results from the query
     * execution.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;X&gt; for some
     * user-defined class X, then the arguments to the multiselect method will
     * be passed to the X constructor and an instance of type X will be returned
     * for each row. The IllegalStateException will be thrown if a constructor
     * for the given argument types does not exist.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;X[]&gt; for some class X,
     * an instance of type X[] will be returned for each row. The elements of
     * the array will correspond to the arguments of the multiselect method. The
     * IllegalStateException will be thrown if the arguments to the multiselect
     * method are not of type X.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Object&gt;, and only a
     * single argument is passed to the multiselect method, an instance of type
     * Object will be returned for each row.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Object&gt;, and more than
     * one argument is passed to the multiselect method, an instance of type
     * Object[] will be instantiated and returned for each row. The elements of
     * the array will correspond to the arguments to the multiselect method.
     *
     * @param selections
     *            expressions specifying the items that are to be returned in
     *            the query result
     * @return the modified query
     */
    @Override
    public CriteriaQuery<T> multiselect(Selection<?>... selections) {
        if (selections == null || selections.length == 0) {
            this.selection = null;
            return this;
        }
        for (Selection select : selections) {
            ((SelectionImpl)select).findRootAndParameters(this);
        }
        if (this.queryResult == ResultType.CONSTRUCTOR) {
            populateAndSetConstructorSelection(null, this.queryType, selections);
        } else if (this.queryResult.equals(ResultType.ENTITY)) {
            if (selections.length == 1 && selections[0].getJavaType().equals(this.queryType)) {
                this.selection = (SelectionImpl<? extends T>) selections[0];
            } else {
                try {
                    populateAndSetConstructorSelection(null, this.queryType, selections);//throws IllegalArgumentException if it doesn't exist
                } catch(IllegalArgumentException constructorDoesNotExist){
                    this.queryResult = ResultType.PARTIAL;
                    this.selection = new CompoundSelectionImpl(this.queryType, selections);
                }
            }
        } else if (this.queryResult.equals(ResultType.TUPLE)) {
            this.selection = new CompoundSelectionImpl(this.queryType, selections);
        } else if (this.queryResult.equals(ResultType.OTHER)) {
            if (selections.length == 1 && selections[0].getJavaType().equals(this.queryType)) {
                this.selection = (SelectionImpl<? extends T>) selections[0];
            } else {
                if (!BasicTypeHelperImpl.getInstance().isDateClass(this.queryType)) {
                    throw new IllegalArgumentException(ExceptionLocalization.buildMessage("MULTIPLE_SELECTIONS_PASSED_TO_QUERY_WITH_PRIMITIVE_RESULT"));
                }
                populateAndSetConstructorSelection(null, this.queryType, selections);
            }
        } else { // unknown
            this.selection = new CompoundSelectionImpl(this.queryType, selections);
        }
        //bug 366386: validate that aliases are not reused
        if (this.selection.isCompoundSelection() && ((CompoundSelectionImpl)this.selection).getDuplicateAliasNames() != null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa_criteriaapi_alias_reused",
                    new Object[] { ((CompoundSelectionImpl)this.selection).getDuplicateAliasNames() }));
        }
        return this;
    }

    /**
     * Specify the items that are to be returned in the query result. Replaces
     * the previously specified selection(s), if any.
     * <p>
     * The type of the result of the query execution depends on the
     * specification of the criteria query object as well as the arguments to
     * the multiselect method as follows:
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Tuple&gt;, a Tuple object
     * corresponding to the items in the selection list passed to the
     * multiselect method will be instantiated and returned for each row that
     * results from the query execution.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;X&gt; for some
     * user-defined class X, then the items in the selection list passed to the
     * multiselect method will be passed to the X constructor and an instance of
     * type X will be returned for each row. The IllegalStateException will be
     * thrown if a constructor for the given argument types does not exist.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;X[]&gt; for some class X,
     * an instance of type X[] will be returned for each row. The elements of
     * the array will correspond to the items in the selection list passed to
     * the multiselect method. The IllegalStateException will be thrown if the
     * elements in the selection list passed to the multiselect method are not
     * of type X.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Object&gt;, and the
     * selection list passed to the multiselect method contains only a single
     * item, an instance of type Object will be returned for each row.
     * <p>
     * If the type of the criteria query is CriteriaQuery&lt;Object&gt;, and the
     * selection list passed to the multiselect method contains more than one
     * item, an instance of type Object[] will be instantiated and returned for
     * each row. The elements of the array will correspond to the items in the
     * selection list passed to the multiselect method.
     *
     * @param selectionList
     *            list of expressions specifying the items that to be are
     *            returned in the query result
     * @return the modified query
     */
    @Override
    public CriteriaQuery<T> multiselect(List<Selection<?>> selectionList) {
        if (selectionList == null) {
            this.selection = null;
            return this;
        }
        return this.multiselect(selectionList.toArray(new Selection[0]));
    }

    // override the return type only:
    @Override
    public CriteriaQuery<T> where(Expression<Boolean> restriction) {
        return (CriteriaQuery<T>) super.where(restriction);
    }

    @Override
    public CriteriaQuery<T> where(Predicate... restrictions) {
        return (CriteriaQuery<T>) super.where(restrictions);
    }

    @Override
    public CriteriaQuery<T> where(List<Predicate> restrictions) {
        return (CriteriaQuery<T>) super.where(restrictions);
    }

    /**
     * Specify the expressions that are used to form groups over the query
     * results. Replaces the previous specified grouping expressions, if any. If
     * no grouping expressions are specified, any previously added grouping
     * expressions are simply removed. This method only overrides the return
     * type of the corresponding AbstractQuery method.
     *
     * @param grouping
     *            zero or more grouping expressions
     * @return the modified query
     */
    @Override
    public CriteriaQuery<T> groupBy(Expression<?>... grouping) {
        super.groupBy(grouping);
        return this;
    }

    /**
     * Specify the expressions that are used to form groups over the query
     * results. Replaces the previous specified grouping expressions, if any. If
     * no grouping expressions are specified, any previously added grouping
     * expressions are simply removed. This method only overrides the return
     * type of the corresponding AbstractQuery method.
     *
     * @param grouping
     *            list of zero or more grouping expressions
     * @return the modified query
     */
    @Override
    public CriteriaQuery<T> groupBy(List<Expression<?>> grouping) {
        super.groupBy(grouping);
        return this;
    }

    @Override
    public CriteriaQuery<T> having(Expression<Boolean> restriction) {
        super.having(restriction);
        return this;
    }

    @Override
    public CriteriaQuery<T> having(Predicate... restrictions) {
        super.having(restrictions);
        return this;
    }

    @Override
    public CriteriaQuery<T> having(List<Predicate> restrictions) {
        super.having(restrictions);
        return this;
    }

    /**
     * Specify the ordering expressions that are used to order the query
     * results. Replaces the previous ordering expressions, if any. If no
     * ordering expressions are specified, the previous ordering, if any, is
     * simply removed, and results will be returned in no particular order. The
     * left-to-right sequence of the ordering expressions determines the
     * precedence, whereby the leftmost has highest precedence.
     *
     * @param o
     *            zero or more ordering expressions
     * @return the modified query.
     */
    @Override
    public CriteriaQuery<T> orderBy(Order... o) {
        this.orderBy = new ArrayList<>();
        for (Order order : o) {
            findRootAndParameters(order);
            this.orderBy.add(order);
        }
        return this;
    }

    /**
     * Specify the ordering expressions that are used to order the query
     * results. Replaces the previous ordering expressions, if any. If no
     * ordering expressions are specified, the previous ordering, if any, is
     * simply removed, and results will be returned in no particular order. The
     * order of the ordering expressions in the list determines the precedence,
     * whereby the first element in the list has highest precedence.
     *
     * @param o
     *            list of zero or more ordering expressions
     * @return the modified query.
     */
    @Override
    public CriteriaQuery<T> orderBy(List<Order> o) {
        for (Order order : o) {
            findRootAndParameters(order);
        }
        this.orderBy = o;
        return this;
    }

    /**
     * This method will set this queryImpl's selection to a ConstructorSelectionImpl, creating a new
     * instance or populating the one passed in as necessary.
     * Throws IllegalArgumentException if a constructor taking arguments represented
     * by the selections array doesn't exist for the given class.
     * <p>
     * Also sets the query result to ResultType.CONSTRUCTOR
     *
     */
    public void populateAndSetConstructorSelection(ConstructorSelectionImpl<T> constructorSelection, Class<T> class1, Selection<?>... selections) throws IllegalArgumentException{
        Class<?>[] constructorArgs = new Class<?>[selections.length];
        int count = 0;
        for (Selection<?> select : selections) {
            if(select instanceof ConstructorSelectionImpl) {
                @SuppressWarnings("unchecked")
                ConstructorSelectionImpl<T> constructorSelect = (ConstructorSelectionImpl<T>)select;
                @SuppressWarnings("unchecked")
                Selection<? extends T>[] selectArray = constructorSelect.getCompoundSelectionItems().toArray(new Selection[0]);
                populateAndSetConstructorSelection(constructorSelect, (Class<T>) constructorSelect.getJavaType(), selectArray);
            }
            constructorArgs[count++] = select.getJavaType();
        }
        Constructor<? extends T> constructor = null;
        try {
            // TODO: Remove AccessController.doPrivileged
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                constructor = AccessController.doPrivileged(new PrivilegedGetConstructorFor<>(class1, constructorArgs, false));
            } else {
                constructor = PrivilegedAccessHelper.getConstructorFor(class1, constructorArgs, false);
            }
            if (constructorSelection == null){
                constructorSelection = new ConstructorSelectionImpl<>(class1, selections);
            }
            this.queryResult = ResultType.CONSTRUCTOR;
            constructorSelection.setConstructor(constructor);
            constructorSelection.setConstructorArgTypes(constructorArgs);
            this.selection = constructorSelection;
        } catch (Exception e){
            //PrivilegedActionException and NoSuchMethodException are possible
            Object[] params = new Object[1];
            params[0] = this.queryType;
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("criteria_no_constructor_found", params), e);
        }
    }

    /**
     * Specify whether duplicate query results will be eliminated. A true value
     * will cause duplicates to be eliminated. A false value will cause
     * duplicates to be retained. If distinct has not been specified, duplicate
     * results must be retained. This method only overrides the return type of
     * the corresponding AbstractQuery method.
     *
     * @param distinct
     *            boolean value specifying whether duplicate results must be
     *            eliminated from the query result or whether they must be
     *            retained
     * @return the modified query.
     */
    @Override
    public CriteriaQuery<T> distinct(boolean distinct) {
        super.distinct(distinct);
        return this;
    }

    @Override
    public void addJoin(FromImpl from) {
        if (this.joins == null) {
            this.joins = new LinkedHashSet<>();
        }
        this.joins.add(from);
    }

    /**
     * Create {@link ReadAllQuery} or {@link ReportQuery} from content of this instance.
     * May be forced to always return {@link ReportQuery}, even when {@link ReadAllQuery} is sufficient.
     * This is required for complex queries with {@code UNION}/{@code EXCEPT}/{@code INTERSECT}, where
     * only {@link ReportQuery} instance is accepted as part of the query.
     *
     * @param toReportQuery force {@link ReportQuery} creation.
     * @return {@link ReadAllQuery} or {@link ReportQuery} from content of this instance
     */
    protected ReadAllQuery getDatabaseQuery(boolean toReportQuery) {
        ReadAllQuery query = null;
        if (this.selection == null || !this.selection.isCompoundSelection()) {
            query = createSimpleQuery(toReportQuery);
        } else {
            query = createCompoundQuery(toReportQuery);
        }
        return query;
    }

    /**
     * Create {@link ReadAllQuery} or {@link ReportQuery} from content of this instance.
     * Returned class depends on current query complexity.
     *
     * @return {@link ReadAllQuery} or {@link ReportQuery} from content of this instance
     */
    @Override
    protected DatabaseQuery getDatabaseQuery() {
        return getDatabaseQuery(false);
    }

    /**
     * Return the ordering expressions in order of precedence.
     *
     * @return the list of ordering expressions
     */
    @Override
    public List<Order> getOrderList() {
        return this.orderBy;
    }

    /**
     * Return the selection item of the query. This will correspond to the query
     * type.
     *
     * @return the selection item of the query
     */
    @Override
    public Selection<T> getSelection() {
        return (Selection<T>) this.selection;
    }

    /**
     * Translates from the criteria query to a EclipseLink Database Query.
     */
    @SuppressWarnings("deprecation")
    protected ReadAllQuery createCompoundQuery(boolean toReportQuery) {
        ReadAllQuery query = null;

        if (this.queryResult == ResultType.UNKNOWN) {
            if (this.selection.isConstructor()) {
                this.queryResult = ResultType.CONSTRUCTOR;
            } else if (this.selection.getJavaType().equals(Tuple.class)) {
                this.queryResult = ResultType.TUPLE;
            } else {
                this.queryResult = ResultType.OBJECT_ARRAY;
            }
        }

        if (this.queryResult.equals(ResultType.PARTIAL)) {
            ReadAllQuery raq;
            @SuppressWarnings("unchecked")
            List<SelectionImpl<?>> selectionItems = (List<SelectionImpl<?>>) (List<?>) this.selection.getCompoundSelectionItems();
            if (toReportQuery) {
                raq = createReportQuery(this.queryType);
                for (SelectionImpl<?> nested : selectionItems) {
                    ((ReportQuery) raq).addAttribute(nested.getAlias(), nested.getCurrentNode(), nested.getJavaType());
                }
            } else {
                raq = new ReadAllQuery(this.queryType);
            }
            // Partial object queries are not allowed to maintain the cache or be edited. Requires dontMaintainCache().
            // See hasPartialAttributeExpressions() in ObjectLevelReadQuery#prepareQuery()
            raq.dontMaintainCache();
            for (SelectionImpl<?> selection : selectionItems) {
                raq.addPartialAttribute((selection).currentNode);
            }
            raq.setExpressionBuilder(((InternalSelection) this.selection.getCompoundSelectionItems().get(0)).getCurrentNode().getBuilder());
            query = raq;
        } else {
            ReportQuery reportQuery = null;
            if (this.queryResult.equals(ResultType.CONSTRUCTOR) || this.queryResult.equals(ResultType.OTHER)) {
                // other is also a constructor type if multi-select was called.
                // with a type other than the query type.
                reportQuery = new ReportQuery();
                reportQuery.addConstructorReportItem(((ConstructorSelectionImpl) this.selection).translate());
                reportQuery.setShouldReturnSingleAttribute(true);
            } else {
                if (this.queryResult.equals(ResultType.TUPLE)) {
                    reportQuery = new TupleQuery(this.selection == null ? new ArrayList<>() : this.selection.getCompoundSelectionItems());
                } else {
                    reportQuery = new ReportQuery();
                    reportQuery.setShouldReturnWithoutReportQueryResult(true);
                }
                reportQuery.setExpressionBuilder(((InternalSelection) this.selection.getCompoundSelectionItems().get(0)).getCurrentNode().getBuilder());
                for (Selection nested : this.selection.getCompoundSelectionItems()) {
                    if (((SelectionImpl) nested).isConstructor()) {
                        reportQuery.addConstructorReportItem(((ConstructorSelectionImpl) nested).translate());
                    } else if (nested.isCompoundSelection()) {
                        throw new IllegalStateException(ExceptionLocalization.buildMessage("NESTED_COMPOUND_SELECTION_OTHER_THAN_CONSTRUCTOR_NOT_SUPPORTED"));
                    } else {
                        if (((InternalSelection) nested).isFrom()) {
                            reportQuery.addItem(nested.getAlias(), ((SelectionImpl) nested).getCurrentNode(), ((FromImpl) nested).findJoinFetches());
                        } else if (((InternalExpression) nested).isCompoundExpression() && ((FunctionExpressionImpl) nested).getOperation() == CriteriaBuilderImpl.SIZE) {
                            //selecting size not all databases support subselect in select clause so convert to count/groupby
                            PathImpl collectionExpression = (PathImpl) ((FunctionExpressionImpl) nested).getChildExpressions().get(0);
                            ExpressionImpl fromExpression = (ExpressionImpl) collectionExpression.getParentPath();
                            reportQuery.addAttribute(nested.getAlias(), collectionExpression.getCurrentNode().count(), ClassConstants.INTEGER);
                            reportQuery.addGrouping(fromExpression.getCurrentNode());
                        } else {
                            reportQuery.addAttribute(nested.getAlias(), ((SelectionImpl) nested).getCurrentNode(), nested.getJavaType());
                        }
                    }
                }
            }

            ExpressionBuilder builder = null;
            Class<?> queryClazz = null;
            // First check the WHERE clause
            if(this.where != null && ((InternalSelection) this.where).getCurrentNode() != null) {
                builder = ((InternalSelection) this.where).getCurrentNode().getBuilder();
                queryClazz = builder.getQueryClass();
            }
            // Check all the SELECTION items next
            if(queryClazz == null && this.selection != null) {
                for(Selection<?> s : this.selection.getCompoundSelectionItems()) {
                    if(((InternalSelection) s).getCurrentNode() != null ) {
                        builder = ((InternalSelection) s).getCurrentNode().getBuilder();
                        queryClazz = builder.getQueryClass();
                        if(queryClazz != null) {
                            break;
                        }
                    }
                }
            }
            // Fallback on the root
            if(queryClazz == null && this.roots != null) {
                for(Root<?> r : this.roots) {
                    if(((RootImpl<?>) r).getCurrentNode() != null ) {
                        builder = ((RootImpl<?>) r).getCurrentNode().getBuilder();
                        queryClazz = builder.getQueryClass();
                        if(queryClazz != null) {
                            break;
                        }
                    }
                }
            }
            reportQuery.setExpressionBuilder(builder);
            reportQuery.setReferenceClass(queryClazz);

            query = reportQuery;
            if (this.groupBy != null && !this.groupBy.isEmpty()) {
                for (Expression<?> exp : this.groupBy) {
                    reportQuery.addGrouping(((InternalSelection) exp).getCurrentNode());
                }
            }
            if (this.havingClause != null) {
                reportQuery.setHavingExpression(((InternalSelection) this.havingClause).getCurrentNode());
            }
        }
        return query;
    }

    protected ReadAllQuery createSimpleQuery(boolean toReportQuery) {
        ReadAllQuery query = null;

        if (this.queryResult == ResultType.UNKNOWN) {
            // unknown type so let's figure this out.
            if (selection == null) {
                if (this.roots != null && !this.roots.isEmpty()) {
                    this.selection = (SelectionImpl<? extends T>) this.roots.iterator().next();
                    if (toReportQuery) {
                        // TODO: Test and fix
                        query = createReportQueryWithItem(((FromImpl) this.selection).getJavaType());
                    } else {
                        query = new ReadAllQuery(((FromImpl) this.selection).getJavaType());
                    }
                    List<org.eclipse.persistence.expressions.Expression> list = ((FromImpl) this.roots.iterator().next()).findJoinFetches();
                    for (org.eclipse.persistence.expressions.Expression fetch : list) {
                        query.addJoinedAttribute(fetch);
                    }
                    if (!list.isEmpty()) {
                        query.setExpressionBuilder(list.get(0).getBuilder());
                    }
                } else if (this.roots == null || this.roots.isEmpty()) {
                    throw new IllegalArgumentException(ExceptionLocalization.buildMessage("CRITERIA_NO_ROOT_FOR_COMPOUND_QUERY"));
                }
            } else {
                // Selection is not null set type to selection
                TypeImpl<? extends T> type = ((MetamodelImpl)this.metamodel).getType(selection.getJavaType());
                if (type != null && type.getPersistenceType().equals(PersistenceType.ENTITY)) {
                    if (toReportQuery) {
                        query = createReportQueryWithItem(type.getJavaType());
                        ((ReportQuery) query).setShouldReturnWithoutReportQueryResult(true);
                    } else {
                        query = new ReadAllQuery(type.getJavaType());
                    }
                    List<org.eclipse.persistence.expressions.Expression> list = ((FromImpl<?, ?>) this.roots.iterator().next()).findJoinFetches();
                    for (org.eclipse.persistence.expressions.Expression fetch : list) {
                        query.addJoinedAttribute(fetch);
                    }
                    query.setExpressionBuilder(((InternalSelection)selection).getCurrentNode().getBuilder());
                } else {
                    query = createReportQueryWithSelection(this.selection.getCurrentNode().getBuilder().getQueryClass());
                }
            }
        } else if (this.queryResult.equals(ResultType.ENTITY)) {
            boolean nonRootSelection = this.selection != null && (!((InternalSelection) this.selection).isRoot());
            if (nonRootSelection) {
                query = createReportQueryWithItem(this.queryType);
                ((ReportQuery) query).setShouldReturnSingleAttribute(true);
            } else {
                // Tested by testUnionAllWithNoSelection
                if (toReportQuery) {
                    query = createReportQueryWithItem(this.queryType);
                    ((ReportQuery) query).setShouldReturnWithoutReportQueryResult(true);
                } else {
                    query = new ReadAllQuery(this.queryType);
                }
            }
            // Union query always needs proper ExpressionBuilder set
            if (!nonRootSelection || isUnion) {
                boolean doSetExpressionBuilder = true;
                if (this.roots != null && !this.roots.isEmpty()) {
                    List<org.eclipse.persistence.expressions.Expression> list = ((FromImpl<?, ?>) this.roots.iterator().next()).findJoinFetches();
                    // Expression builder from root selection
                    if (!list.isEmpty()) {
                        // Set the builder to one of the fetches bases
                        query.setExpressionBuilder(list.get(0).getBuilder());
                        doSetExpressionBuilder = false;
                    }
                    if (this.selection == null || ((InternalSelection) this.selection).isRoot()) {
                        for (org.eclipse.persistence.expressions.Expression fetch : list) {
                            query.addJoinedAttribute(fetch);
                        }
                    }
                }
                // Set the builder to non-root selection as a fallback
                if (doSetExpressionBuilder && selection != null) {
                    query.setExpressionBuilder(this.selection.currentNode.getBuilder());
                }
            }
        } else {
            ReportQuery reportQuery = null;
            if (this.queryResult.equals(ResultType.TUPLE)) {
                List<Object> list = new ArrayList<>();
                list.add(this.selection);
                reportQuery = new TupleQuery(list);
            } else {
                // Tested by testUnionWithEntityParameterInSelection
                reportQuery = new ReportQuery();
                reportQuery.setShouldReturnWithoutReportQueryResult(true);
            }
            if (this.selection != null) {
                if (!this.selection.isCompoundSelection() && ((InternalExpression)this.selection).isCompoundExpression()){
                    if(((FunctionExpressionImpl)this.selection).getOperation() == CriteriaBuilderImpl.SIZE){
                    //selecting size not all databases support subselect in select clause so convert to count/groupby
                    PathImpl collectionExpression = (PathImpl) ((FunctionExpressionImpl)this.selection).getChildExpressions().get(0);
                    ExpressionImpl fromExpression = (ExpressionImpl) collectionExpression.getParentPath();
                    reportQuery.addAttribute(this.selection.getAlias(), collectionExpression.getCurrentNode().count(), ClassConstants.INTEGER);
                    reportQuery.addGrouping(fromExpression.getCurrentNode());
                }else{
                    reportQuery.addAttribute(this.selection.getAlias(), this.selection.getCurrentNode(), this.selection.getJavaType());

                }}else{
                if (((InternalSelection) selection).isFrom()) {
                    reportQuery.addItem(selection.getAlias(), selection.getCurrentNode(), ((FromImpl) selection).findJoinFetches());
                } else {
                    reportQuery.addAttribute(selection.getAlias(), selection.getCurrentNode(), selection.getJavaType());
                }}
                reportQuery.setReferenceClass(((InternalSelection) this.selection).getCurrentNode().getBuilder().getQueryClass());
                reportQuery.setExpressionBuilder(((InternalSelection) this.selection).getCurrentNode().getBuilder());
            }
            query = reportQuery;
            if (this.groupBy != null && !this.groupBy.isEmpty()) {
                for (Expression<?> exp : this.groupBy) {
                    reportQuery.addGrouping(((InternalSelection) exp).getCurrentNode());
                }
            }
            if (this.havingClause != null) {
                reportQuery.setHavingExpression(((InternalSelection) this.havingClause).getCurrentNode());
            }
        }
        if (query.getReferenceClass() == null){
            if (this.where != null && ((InternalSelection) this.where).getCurrentNode() != null && ((InternalSelection) this.where).getCurrentNode().getBuilder() != null && ((InternalSelection) this.where).getCurrentNode().getBuilder().getQueryClass() != null) {
                query.setReferenceClass(((InternalSelection) this.where).getCurrentNode().getBuilder().getQueryClass());
            } else if (roots != null && ! roots.isEmpty()){
                Root<?> root = this.getRoots().iterator().next();
                query.setReferenceClass(root.getJavaType());
            }
        }

        if (selection == null) {
            //the builder in the where clause  may not be the correct builder for this query.  Search for a root that matches the query type.
            if (roots != null && ! roots.isEmpty()){
                for (Root root : this.getRoots()){
                    if (root.getJavaType().equals(this.queryType)){
                        query.setExpressionBuilder(((RootImpl) root).getCurrentNode().getBuilder());
                        break;
                    }
                }
            }
        }

        return query;
    }

    // Create ReportQuery with provided reference class
    private static ReportQuery createReportQuery(Class<?> classToRead) {
        ReportQuery query = new ReportQuery();
        query.setReferenceClass(classToRead);
        return query;
    }

    // Create ReportQuery with provided reference class
    // Add single result expression value
    private ReportQuery createReportQueryWithItem(Class<?> classToRead) {
        ReportQuery query = createReportQuery(classToRead);
        if (selection != null) {
            if (((InternalSelection) selection).isFrom()) {
                query.addItem(selection.getAlias(), selection.getCurrentNode(), ((FromImpl<?, ?>) selection).findJoinFetches());
            } else {
                query.addAttribute(selection.getAlias(), selection.getCurrentNode(), selection.getJavaType());
            }
        } else {
            FromImpl<?, ?> root = ((FromImpl<?, ?>) roots.iterator().next());
            query.addItem(root.getAlias(), root.getCurrentNode());
        }
        return query;
    }

    // Create ReportQuery with provided reference class
    // Selection exists (selection != null) and may be compound or simple
    private ReportQuery createReportQueryWithSelection(Class<?> classToRead) {
        ReportQuery query = createReportQuery(classToRead);
        // Just to blame for invalid usage
        if (selection == null) {
            throw new IllegalStateException("Called createReportQueryWithSelection with no selection set");
        }
        if (!selection.isCompoundSelection() && ((InternalExpression) selection).isCompoundExpression()) {
            // noinspection StringEquality - using String constants so instances will match
            if (((FunctionExpressionImpl<?>) selection).getOperation() == CriteriaBuilderImpl.SIZE) {
                // Selecting size not all databases support sub-select in select clause so convert to count/groupby
                PathImpl<?> collectionExpression = (PathImpl<?>) ((FunctionExpressionImpl<?>) this.selection).getChildExpressions().get(0);
                ExpressionImpl<?> fromExpression = (ExpressionImpl<?>) collectionExpression.getParentPath();
                query.addAttribute(this.selection.getAlias(), collectionExpression.getCurrentNode().count(), ClassConstants.INTEGER);
                query.addGrouping(fromExpression.getCurrentNode());
            }
            query.addAttribute(this.selection.getAlias(), this.selection.getCurrentNode(), this.selection.getJavaType());
        } else {
            query.addItem(this.selection.getAlias(), this.selection.getCurrentNode());
            query.setShouldReturnSingleAttribute(true);
        }
        return query;
    }

    /**
     * Translates from the criteria query to a EclipseLink Database Query.
     */
    @Override
    public DatabaseQuery translate() {
        return translate(false);
    }

    /**
     * Translate always to {@link ReportQuery}.
     */
    public ReportQuery transalteToReportQuery() {
        return (ReportQuery) translate(true);
    }

    private ReadAllQuery translate(boolean toReportQuery) {
        ReadAllQuery query = (ReadAllQuery) translate(getDatabaseQuery(toReportQuery));

        for (Iterator<Root<?>> iterator = this.getRoots().iterator(); iterator.hasNext();) {
            findJoins((FromImpl) iterator.next());
        }

        if (this.joins != null && !joins.isEmpty()) {
            for (FromImpl join : this.joins) {
                query.addNonFetchJoinedAttribute(((InternalSelection) join).getCurrentNode());
            }
        }
        if (this.distinct) {
            query.setDistinctState(ObjectLevelReadQuery.USE_DISTINCT);
        } else {
            query.setDistinctState(ObjectLevelReadQuery.DONT_USE_DISTINCT);
            if (query.hasJoining()) {
                query.setShouldFilterDuplicates(false);
            }
        }
        if (this.orderBy != null && !this.orderBy.isEmpty()) {
            for (Order order : this.orderBy) {
                OrderImpl orderImpl = (OrderImpl) order;
                org.eclipse.persistence.expressions.Expression orderExp = ((ExpressionImpl<?>) orderImpl.getExpression()).getCurrentNode();
                orderExp = orderImpl.isAscending() ? orderExp.ascending() : orderExp.descending();
                orderExp = switch (orderImpl.getNullPrecedence()) {
                    case FIRST -> orderExp.nullsFirst();
                    case LAST -> orderExp.nullsLast();
                    default -> orderExp;
                };
                query.addOrdering(orderExp);
            }
        }

        return query;
    }

    /**
     * Mark this query as part of the UNION/EXCEPT/INTERSECT.
     * This will trigger ExpressionBuilder to be added to the ReportQuery during {@link #getDatabaseQuery(boolean)}
     * execution.
     */
    void isUnion() {
        isUnion = true;
    }

}
